# -*- coding:utf-8 -*-
from concurrent.futures import ThreadPoolExecutor
from unittest.suite import _isnotsuite
from types import MethodType
from Util.tools import log, is_null
from Common.test_func import generate_report, send_DD_for_FXC, send_warning_after_test, is_exist_start_case, \
    stop_case_run_status, start_case_run_status
from Util.decorator_tools import async
import threading, time


"""
 [ 动态修改 suite.py 文件中 TestSuite 类中的 run 方法 ]

 def run(self, result, debug=False) 
    ......... 
    for index, test in enumerate(self):
        .........
        test(result) 
        .........

 self ：
 -> 表示 suite 实例对象（包含了所有的测试用例实例，即继承了'unittest.TestCase'的子类的实例对象 test_instance ）
 <unittest.suite.TestSuite tests=[<test_case.train_test.TrainTest testMethod=test_01>, 
                                  <test_case.train_test.TrainTest testMethod=test_02>, 
                                  <test_case.train_test.TrainTest testMethod=test_baidu>]>

 result:
 -> 表示 result.py 文件中的 TestResult 类的 实例对象

 test(result) ：等同于 test_instance(result)
 -> 表示 调用'unittest.TestCase'中的__call__方法执行该类中的 run 方法
 -> 理解：实例对象'test'通过'__call__'方法将自己变成一个函数来调用

 testResult = test(result)  ->  <class 'TestBase.HTMLTestReport._TestResult'>
 testResult.result          ->  [ (n,t,o,e), (n,t,o,e), ... ]
    n: 0->通过，1->失败，2->错误
    t: 测试实例对象
    o: 测试过程中输出的内容（一般都是空的）
    e: 错误信息、失败的信息
  [ (1, <Project.pro_demo_1.test_case.train_test.TrainTest testMethod=test_01>, '', ''), ...... ]
            
"""


def exec_test_with_retry(test, result, try_limit=3, interval_time=3):
    """
        执行测试用例（ 失败重试3次，间隔3秒 ）
        :param test: 表示'测试用例'实例对象
        :param result: 测试结果报告 （ TestBase > HTMLTestReport > _TestResult 实例对象）

        < 注意点 >
            testResult = test(result)  执行用例，并将测试结果依次保存在'result'中（ 即：'_TestResult'实例对象中 ）

        < 重试逻辑 >
            1.重试过程中，需要清理的记录：
             （1）删除测试结果中相应的记录
             （2）将相应的统计结果减一
             （3）删除已经保存的截图
            2.当最后一次也失败时，不要删除测试结果记录
    """
    try_cnt = 0  # 重试计数
    while try_cnt < try_limit:
        testResult = test(result)  # 执行测试用例（ 下一步的快捷查看：unittest > TestCase(case.py) > run (Line 570) ）

        del_i = None  # 当前用例在测试结果列表中的索引（记录待删除的索引）
        exec_res = None  # 当前用例的执行结果 0->通过，1->失败，2->错误
        for i, res_tuple in enumerate(testResult.result):
            if res_tuple[1].test_method == test.test_method:  # 通过'用例方法名称'找到当前用例
                exec_res = res_tuple[0]
                del_i = i

        # 若执行结果不是通过，则清除相应的记录
        if exec_res != 0:
            if try_cnt < try_limit - 1:  # 目的：当最后一次也失败时，不要删除测试结果记录
                test.screen_shot_id_list = []
                del testResult.result[del_i]
                if exec_res == 1:
                    testResult.failure_count -= 1
                elif exec_res == 2:
                    testResult.error_count -= 1
            time.sleep(interval_time)
            try_cnt += 1
        else:  # 若执行结果通过，则直接跳出循环
            break


def run_test_custom(self, test, result, debug, index):
    """
    :param self: 表示'suit'实例对象
    :param test: 表示'测试用例'实例对象
    :param result: 测试结果报告 （ TestBase > HTMLTestReport > _TestResult 实例对象）
    :param debug:
    :param index:
    :return:

        多线程中执行的内容
           1.需要为实例对象'suite'<TestSuite>动态添加该方法
           2.目的：供多线程中调用
    """
    # 启动测试用例：设置用例的'运行状态=running'和'开始时间'
    start_case_run_status(pro_name=test.pro_name, test_method_name=test.test_method)

    # 获取当前线程名称 -> ThreadPoolExecutor-1_0
    thread_name = threading.currentThread().getName()
    # 获取当前线程名称索引+1，并赋值给实例对象的属性
    test.thread_name_index = int(thread_name.split("_")[1]) + 1

    if not debug:
        exec_test_with_retry(test, result)  # 执行测试用例（默认：失败重试3次）
    else:
        test.debug()

    # (self)实例对象'suite'<TestSuite> 为每个执行完毕的(test)'测试用例'实例 保存'截图ID列表'
    self.screen_shot_id_dict[test.screen_shot_id_list_name] = test.screen_shot_id_list

    if self._cleanup:
        self._removeTestAtIndex(index)

    # 停止测试用例：设置用例的'运行状态=stopping'和'运行时间'
    stop_case_run_status(pro_name=test.pro_name, test_method_name=test.test_method)

    # 返回值<'tuple'>：返回测试用例实例对象的两个属性值（ 项目名称、测试方法名 ）供回调函数使用
    return test.pro_name, test.test_method


def show_result_custom(res):
    """
    :param res: 某个线程执行完毕后的返回结果
    :return:

     多线程回调函数
      1.需要为实例对象'suite'<TestSuite>动态添加该方法
      2.目的：供多线程中调用
    """

    # 停止测试用例：设置用例的'运行状态=stopping'和'运行时间'
    result = res.result()
    stop_case_run_status(pro_name=result[0], test_method_name=result[1])


def new_run(self, result, debug=False):
    """
    :param self: 表示'suit'实例对象
    :param result: 测试结果报告
    :param debug:
    :return:

     动态修改'suite.py'文件中'TestSuite'类中的'run'方法
      1.为实例对象'suite'<TestSuite>动态修改实例方法'run'
      2.目的：启用多线程来执行case
    """
    topLevel = False
    if getattr(result, '_testRunEntered', False) is False:
        result._testRunEntered = topLevel = True

    # 多线程执行测试
    pool = ThreadPoolExecutor(self.thread_num)
    for index, test in enumerate(self):
        if result.shouldStop:
            break

        if _isnotsuite(test):
            self._tearDownPreviousClass(test, result)
            self._handleModuleFixture(test, result)
            self._handleClassSetUp(test, result)
            result._previousTestClass = test.__class__

            if (getattr(test.__class__, '_classSetupFailed', False) or
                    getattr(result, '_moduleSetUpFailed', False)):
                continue

        """ 启用多线程 调用方法 """
        pool.submit(run_test_custom, self, test, result, debug, index).add_done_callback(show_result_custom)

    """ 等待所有线程执行完毕 """
    pool.shutdown()

    log.info("线程全部执行完毕")

    if topLevel:
        self._tearDownPreviousClass(None, result)
        self._handleModuleTearDown(result)
        result._testRunEntered = False
    return result


@async
def suite_sync_run_case(pro_name, browser_name, thread_num=2, remote=False):
    """
    同时执行不同用例（ 通过动态修改'suite.py'文件中'TestSuite'类中的'run'方法，使得每个线程中的结果都可以记录到测试报告中 ）
    :param pro_name: 项目名称
    :param browser_name: 浏览器名称
    :param thread_num: 线程数
    :param remote: 是否远程执行
       【 备 注 】
      1.suite 实例对象（包含了所有的测试用例实例，即继承了'unittest.TestCase'的子类的实例对象 test_instance ）
      2.开启浏览器操作（每个用例执行一次）：在每个'测试类'的 setUp 方法中执行 ( 继承 ParaCase 父类 )
      3.关闭浏览器操作（每个用例执行一次）：在每个'测试类'的 tearDown 方法中执行 ( 继承 ParaCase 父类 )

       【 保 存 截 屏 图 片 ID 的 逻 辑 】
      1.为实例对象'suite'<TestSuite>动态添加一个属性'screen_shot_id_dict' -> screen_shot_id_dict = {}
      2.每个测试方法中将所有截屏ID都保存入'screen_shot_id_list' -> screen_shot_id_list = ['aaa', 'bbb', 'ccc']
      3.实例对象'suite'在重写的'new_run'方法中 将'screen_shot_id_list'添加入'screen_shot_id_dict'
      4.screen_shot_id_dict = { "测试类名.测试方法名":['aaa', 'bbb'], "测试类名.测试方法名":['cccc'] }
    """
    if is_exist_start_case(pro_name):
        send_DD_for_FXC(title=pro_name, text="#### '" + pro_name + "' 项目存在<运行中>的用例而未执行测试（定时任务）")
        return "Done"

    # 将'测试类'中的所有'测试方法'添加到 suite 对象中（每个'测试类'实例对象包含一个'测试方法'）
    from TestBase.test_case_unit import ParaCase
    suite, on_line_test_method_name_list = ParaCase.get_online_case_to_suite(pro_name=pro_name, browser_name=browser_name, remote=remote)

    if suite != "mongo error":
        if is_null(on_line_test_method_name_list):
            send_DD_for_FXC(title=pro_name, text="#### '" + pro_name + "' 项目<没有上线>的用例而未执行测试（定时任务）")
        else:
            # 为实例对象'suite'<TestSuite>动态添加一个属性'screen_shot_id_dict'（目的：保存截图ID）
            setattr(suite, "screen_shot_id_dict", {})

            # 为实例对象'suite'<TestSuite>动态添加一个属性'thread_num'（目的：控制多线程数量）
            setattr(suite, "thread_num", thread_num)

            # 为实例对象'suite'<TestSuite>动态添加三个方法（ 目的：供多线程中调用 ）
            # 'exec_test_with_retry'、'run_test_custom'、'show_result_custom'
            suite.exec_test_with_retry = MethodType(exec_test_with_retry, suite)
            suite.run_test_custom = MethodType(run_test_custom, suite)
            suite.show_result_custom = MethodType(show_result_custom, suite)

            # 为实例对象'suite'<TestSuite>动态修改实例方法'run'（ 目的：启用多线程来执行case ）
            suite.run = MethodType(new_run, suite)

            # 运行测试，并生成测试报告
            test_result, current_report_file = generate_report(pro_name=pro_name, suite=suite, title='WEB自动化测试报告 - ' + pro_name,
                                                               description='详细测试用例结果', tester="自动化测试", verbosity=2)

            # 测试后发送预警
            send_warning_after_test(pro_name, test_result, current_report_file)


if __name__ == "__main__":
    suite_sync_run_case(pro_name="pro_demo_1", browser_name="Chrome", thread_num=3, remote=False)
    # suite_sync_run_case(pro_name="pro_demo_1", browser_name="Firefox", thread_num=3, remote=False)

